package mlpEnsemble;

import genetic_algorithm.Chromosome;
import genetic_algorithm.Crossover;
import genetic_algorithm.Mutation;

import java.util.AbstractMap;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.Map.Entry;

import utils.RandomGenerator;

public class MlpEnsembleCrossover implements Crossover {

	public static final int SINGLE_POINT = 0; // single point crossover
	public static final int DOUBLE_POINT = 1; // double point crossover
	public static final int UNIFORM = 2; // uniform crossover
	private int mode; // crossover mode
	
	/**
	 * Constructor- creates a new object implementing crossover phase for the
	 * digit recognition problem. Crossover is performed according to given crossover mode:
	 * single point, double point or uniform
	 * @param mode mode for the crossover
	 */
	public MlpEnsembleCrossover(int mode) {
		// set crossover mode
		this.mode = mode;
	}
	
	/**
	 * Gets a pair of parent chromosomes. In chance of crossover-rate, mates
	 * parents (according to the mode which was specified during the
	 * initialization) and mutates them in chance of mutation-rate. If crossover
	 * is not performed, parents are not changed and returned as they are
	 */
	@Override
	public Entry<Chromosome, Chromosome> mate(
			Entry<Chromosome, Chromosome> parents, double crossoverRate,
			Mutation mutator, double mutationRate) {

		// skip crossover in chance of crossover-rate
		if ((RandomGenerator.nextInt(1000) / 1000.0) > crossoverRate) {
			return parents;
		}
		
		// perform crossover according to selected mode
		List<Chromosome> offsprings = null;
		switch (mode) {
		case SINGLE_POINT: {
			offsprings = singlePoint((MlpEnsembleChromosome) parents.getKey(),
					(MlpEnsembleChromosome) parents.getValue());
			break;
		}
		case DOUBLE_POINT: {
			offsprings = doublePoint((MlpEnsembleChromosome) parents.getKey(),
					(MlpEnsembleChromosome) parents.getValue());
			break;
		}
		case UNIFORM: {
			offsprings = uniform((MlpEnsembleChromosome) parents.getKey(),
					(MlpEnsembleChromosome) parents.getValue());
			break;
		}

		default:
			break;
		}
		
		// mutate the offsprings
		mutator.mutate(offsprings, mutationRate);

		// return resulting offsprings
		return new AbstractMap.SimpleEntry<Chromosome, Chromosome>(
				offsprings.get(0), offsprings.get(1));
	}
	
	/**
	 * Performs crossover on given parent chromosomes in a single-point manner:
	 * randomly picks an index in the parents' list values; generates first
	 * offspring by concatenating father's prefix with mother's postfix;
	 * generates second offspring by concatenating mother's prefix with father's
	 * postfix
	 * @param father first parent chromosome
	 * @param mother second parent chromosome
	 * @return list containing offsprings chromosmes
	 */
	private List<Chromosome> singlePoint(MlpEnsembleChromosome father, MlpEnsembleChromosome mother) {

		// initialize values list for offspring chromosomes
		LinkedList<int[]> firstOffspringVals = new LinkedList<int[]>();
		LinkedList<int[]> secondOffspringVals = new LinkedList<int[]>();

		// randomly choose split index
		int numValues = father.getAllValues().size();
		int splitIndex = RandomGenerator.nextInt(numValues);

		// extract prefix and postfix of chromosomes' values
		List<int[]> fatherPrefix = new LinkedList<int[]>(father.getAllIndices().subList(0, splitIndex));
		List<int[]> fatherPostfix = new LinkedList<int[]>(father.getAllIndices().subList(splitIndex,numValues));
		List<int[]> motherPrefix = new LinkedList<int[]>(mother.getAllIndices().subList(0, splitIndex));
		List<int[]> motherPostfix = new LinkedList<int[]>(mother.getAllIndices().subList(splitIndex,numValues));

		// concatenate father's prefix with mother's postfix
		firstOffspringVals.addAll(fatherPrefix);
		firstOffspringVals.addAll(motherPostfix);

		// concatenate mother's prefix with father's postfix
		secondOffspringVals.addAll(motherPrefix);
		secondOffspringVals.addAll(fatherPostfix);

		// generate chromosomes out of offsprings values		
		MlpEnsembleChromosome firstOffspring = new MlpEnsembleChromosome(firstOffspringVals);
		MlpEnsembleChromosome secondOffspring = new MlpEnsembleChromosome(secondOffspringVals);

		// return resulting chromosomes
		List<Chromosome> offsprings = new ArrayList<Chromosome>();
		offsprings.add(firstOffspring);
		offsprings.add(secondOffspring);
		return offsprings;
	}
	
	/**
	 * Performs crossover on given parent chromosomes in a double-point manner:
	 * randomly picks two indices in the parents' list values; generates first
	 * offspring by concatenating father's first sublist with mother's second
	 * sublist and father's third sublist; generates second offspring by
	 * concatenating mother's first sublist with father's second sublist and
	 * mother's third sublist 
	 * @param father first parent chromosome
	 * @param mother second parent chromosome
	 * @return list containing offsprings chromosmes
	 */
	private List<Chromosome> doublePoint(MlpEnsembleChromosome father, MlpEnsembleChromosome mother) {

		// split and merge parents until valid offsprings are generated
		LinkedList<int[]> firstOffspringVals = new LinkedList<int[]>();
		LinkedList<int[]> secondOffspringVals = new LinkedList<int[]>();

		// randomly choose split indices
		int numValues = father.getAllValues().size();
		int firstSplit = RandomGenerator.nextInt(numValues - 1);
		int secondSplit = RandomGenerator.nextInt(numValues - firstSplit)
				+ firstSplit;

		// extract sublists of each chromosome's values
		List<int[]> fatherFirst = new LinkedList<int[]>(father.getAllIndices().subList(0,firstSplit));
		List<int[]> fatherSecond = new LinkedList<int[]>(father.getAllIndices().subList(firstSplit, secondSplit));
		List<int[]> fatherThird = new LinkedList<int[]>(father.getAllIndices().subList(secondSplit, numValues));
		List<int[]> motherFirst = new LinkedList<int[]>(mother.getAllIndices().subList(0,firstSplit));
		List<int[]> motherSecond = new LinkedList<int[]>(mother.getAllIndices().subList(firstSplit, secondSplit));
		List<int[]> motherThird = new LinkedList<int[]>(mother.getAllIndices().subList(secondSplit, numValues));

		// concatenate father and mother's sublists alternately
		firstOffspringVals.addAll(fatherFirst);
		firstOffspringVals.addAll(motherSecond);
		firstOffspringVals.addAll(fatherThird);
		secondOffspringVals.addAll(motherFirst);
		secondOffspringVals.addAll(fatherSecond);
		secondOffspringVals.addAll(motherThird);

		// generate chromosomes out of offsprings values
		MlpEnsembleChromosome firstOffspring = new MlpEnsembleChromosome(firstOffspringVals);
		MlpEnsembleChromosome secondOffspring = new MlpEnsembleChromosome(secondOffspringVals);

		// return resulting chromosomes
		List<Chromosome> offsprings = new ArrayList<Chromosome>();
		offsprings.add(firstOffspring);
		offsprings.add(secondOffspring);
		return offsprings;
	}
	
	private List<Chromosome> uniform(MlpEnsembleChromosome father, MlpEnsembleChromosome mother) {
		
		// initialize values list for offspring chromosomes
		LinkedList<int[]> firstOffspringVals = new LinkedList<int[]>();
		LinkedList<int[]> secondOffspringVals = new LinkedList<int[]>();

		// fill values
		int numValues = father.getAllValues().size();
		int isFather; // indicates if choosing from father the current value for first offspring
		for (int i = 0 ; i < numValues ; ++i) {
			
			// choose randomly the parent to inherit its current value
			isFather = RandomGenerator.nextInt(2);
			if (isFather == 0) {
				firstOffspringVals.add(father.getValue(i));
				secondOffspringVals.add(mother.getValue(i));
			} else {
				firstOffspringVals.add(mother.getValue(i));
				secondOffspringVals.add(father.getValue(i));
			}
		}
		
		// generate chromosomes out of offsprings values
		MlpEnsembleChromosome firstOffspring = new MlpEnsembleChromosome(firstOffspringVals);
		MlpEnsembleChromosome secondOffspring = new MlpEnsembleChromosome(secondOffspringVals);

		// return resulting chromosomes
		List<Chromosome> offsprings = new ArrayList<Chromosome>();
		offsprings.add(firstOffspring);
		offsprings.add(secondOffspring);
		return offsprings;
	}
}
